#include "greyscalecode.h"
#include "greyscalecode_android.h"
#include <cstddef>
#include <stdexcept>

#include <newcode/GreyscaleCodeReader.h>
#include <zxing/NotFoundException.h>

#ifdef ANDROID
#	include <android/log.h>
#	define log(msg) __android_log_print(ANDROID_LOG_INFO, "greyscalecodelib", msg)
#else
#	include <cstdio>
#	define log(msg) printf("greyscalecodelib: %s", msg)
#endif

using namespace newcode;
using namespace std;
using namespace zxing;

static const char* const DECODE_RESULT_CLASS =
		"by/bsu/fami/greyscalecodelibrary/GreysacleCodeLibrary$DecodeResult";
static const char* const NOT_FOUND_EXCEPTION_CLASS =
		"by/bsu/fami/greyscalecodelibrary/NotFoundException";
static const char* const READER_EXCEPTION_CLASS =
		"by/bsu/fami/greyscalecodelibrary/ReaderException";
static const char* const ILLEGAL_STATA_EXCEPTION_CLASS = 
		"java/lang/IllegalStateException";

static Ref<ByteMatrix> java2DArrayToByteMatrix(
		JNIEnv *env, jobjectArray matrix);
static void javaThrow(JNIEnv *env, const char* clazz, const char* message);
static jobject returnResult(JNIEnv *env, Ref<ByteMatrix> image);


JNIEXPORT jobject JNICALL
Java_by_bsu_fami_greyscalecodelibrary_GreysacleCodeLibrary_decode
  (JNIEnv *env, jclass cls, jobjectArray image)
{
	(void) cls;
	return returnResult(env, java2DArrayToByteMatrix(env, image));
}

JNIEXPORT jobjectArray JNICALL
Java_by_bsu_fami_greyscalecodelibrary_GreysacleCodeLibrary_encode__Ljava_lang_String_2I
  (JNIEnv *env, jclass cls, jstring data, jint encodeMode)
{
	(void) env; (void) cls; (void)data; (void) encodeMode;
	javaThrow(env, ILLEGAL_STATA_EXCEPTION_CLASS, "Not implemented yet.");
	return NULL;
}

JNIEXPORT jobjectArray JNICALL
Java_by_bsu_fami_greyscalecodelibrary_GreysacleCodeLibrary_encode___3BI
  (JNIEnv *env, jclass cls, jbyteArray data, jint encodeMode)
{
	(void) env; (void) cls; (void)data; (void) encodeMode;
	javaThrow(env, ILLEGAL_STATA_EXCEPTION_CLASS, "Not implemented yet.");
	return NULL;
}

Ref<ByteMatrix> java2DArrayToByteMatrix(JNIEnv *env, jobjectArray matrix)
{
	jsize h = env->GetArrayLength(matrix);
	jsize w = env->GetArrayLength(
			(jbyteArray)env->GetObjectArrayElement(matrix, 0));
	Ref<ByteMatrix> resultMatrix(new ByteMatrix(w, h));
	for (int i = 0; i < h; i++)
	{
		jbyteArray javaRow = (jbyteArray)env->GetObjectArrayElement(matrix, i);
		jbyte* row = env->GetByteArrayElements(javaRow, NULL);
		resultMatrix->setRow((unsigned char*)row, i);
		env->ReleaseByteArrayElements(javaRow, row, JNI_ABORT);
		env->DeleteLocalRef(javaRow);
	}
	return resultMatrix;
}

void javaThrow(JNIEnv *env, const char* clazz, const char* message)
{
	jclass javaException = env->FindClass(clazz);
	if (javaException == NULL)
	{
		log("Failed to load exception class");
	}
	env->ThrowNew(javaException, message);
}

static jobject returnResult(JNIEnv *env, Ref<ByteMatrix> image) {
	try {
		GreyscaleCodeReader reader;
		Ref<Result> result = reader.decode(image);

		jclass resultClass = env->FindClass(DECODE_RESULT_CLASS);
		if (resultClass == NULL)
		{
			throw invalid_argument ("Could not find class DecodeResult");
		}

		jmethodID contructorId = env->GetMethodID(resultClass, "<init>", "()V");
		if(contructorId == NULL)
		{
			throw invalid_argument("Could not find constructor");
		}

		jobject javaResult = env->NewObject(resultClass, contructorId);
		if (javaResult == NULL)
		{
			throw runtime_error("Could not create result object");
		}

		jfieldID textID = env->GetFieldID(resultClass, 
			"text", "Ljava/lang/String;");
		jfieldID rawByresID = env->GetFieldID(resultClass, "rawBytes", "[B");
		jfieldID pointsID = env->GetFieldID(resultClass, "points", "[F");
		if (textID == NULL || rawByresID == NULL || pointsID == NULL)
		{
			throw invalid_argument("Could not find one of fields");
		}

		jstring javaText = NULL;
		if (result->getText() != NULL)
		{
			javaText = env->NewStringUTF(
					result->getText()->getText().c_str());
			if (javaText == NULL)
			{
				throw runtime_error("Could not assign text field");
			}
		}
		jbyteArray javaRawBytes = env->NewByteArray(
			result->getRawBytes()->size());
		env->SetByteArrayRegion(javaRawBytes, 0, result->getRawBytes()->size(),
				(jbyte*)&(result->getRawBytes()[0]));
		jfloatArray javaPoints = env->NewFloatArray(8);
		jfloat points[8];
		for (int i = 0; i < 4; i++)
		{
			points[2 * i] = result->getResultPoints()[i]->getX();
			points[2 * i + 1] = result->getResultPoints()[i]->getY();
		}
		env->SetFloatArrayRegion(javaPoints, 0, 8, points);
		if (javaRawBytes == NULL || javaPoints == NULL)
		{
			throw runtime_error("Could not create rawBytes or points filed");
		}
		env->SetObjectField(javaResult, textID, javaText);
		env->SetObjectField(javaResult, rawByresID, javaRawBytes);
		env->SetObjectField(javaResult, pointsID, javaPoints);

		return javaResult;
	}
	catch (NotFoundException &e)
	{
		javaThrow(env, NOT_FOUND_EXCEPTION_CLASS, e.what());
	}
	catch (exception &e)
	{
		javaThrow(env, READER_EXCEPTION_CLASS, e.what());
	}
	catch(...)
	{
		javaThrow(env, READER_EXCEPTION_CLASS, "Unknown error");
	}
	return NULL;
}

#ifdef ANDROID

#include <android/bitmap.h>

JNIEXPORT jobject JNICALL
Java_by_bsu_fami_greyscalecodelibrary_android_GCLibAndroid8_decode
  (JNIEnv *env, jclass clazz, jobject jbitmap)
{
	(void) clazz;
	AndroidBitmapInfo info;
	int errCode = AndroidBitmap_getInfo(env, jbitmap, &info);
	if (errCode != ANDROID_BITMAP_RESULT_SUCCESS)
	{
		javaThrow(env, READER_EXCEPTION_CLASS, "Could not get Android Bitmap");
	}
	Ref<ByteMatrix> image(new ByteMatrix(info.width, info.height));
	unsigned char* raw;
	errCode = AndroidBitmap_lockPixels(env, jbitmap, (void**)&raw);
	if (errCode != ANDROID_BITMAP_RESULT_SUCCESS)
	{
		javaThrow(env, READER_EXCEPTION_CLASS, 
			"Could not access Android Bitmap");
	}

	switch((AndroidBitmapFormat)info.format)
	{
	
	case ANDROID_BITMAP_FORMAT_NONE:
		javaThrow(env, READER_EXCEPTION_CLASS, "Unknow bitmap format");
		break;
		
	case ANDROID_BITMAP_FORMAT_RGBA_8888:
		for(size_t x = 0; x < info.width; x++)
		{
			for(size_t y = 0; y < info.height; y++)
			{
				int r = raw[y * info.stride + x * 4];
				int g = raw[y * info.stride + x * 4 + 1];
				int b = raw[y * info.stride + x * 4 + 2];
				(*image)(x, y) = (r * 11 + b * 16 + g * 5) / 32;
			}
		}
		break;
		
	case ANDROID_BITMAP_FORMAT_RGB_565:
		for(size_t x = 0; x < info.width; x++)
		{
			for(size_t y = 0; y < info.height; y++)
			{
				int r = (raw[y * info.stride + x * 2] & 0xF8) >> 3;
				int g = ((raw[y * info.stride + x * 2] & 0x7) << 3) |
						((raw[y * info.stride + x * 2 + 1] & 0xE0) >> 5);
				int b = raw[y * info.stride + x * 2 + 1] & 0x1F;
				(*image)(x, y) = (r * 11 + b * 16 + g * 5) / 32;
			}
		}
		break;

	case ANDROID_BITMAP_FORMAT_RGBA_4444:
		for (size_t x = 0; x < info.width; x++)
		{
			for (size_t y = 0; y < info.height; y++)
			{
				int r = (raw[y * info.stride + x * 2] & 0xF0) >> 4;
				int g = raw[y * info.stride + x * 2] & 0xF;
				int b = (raw[y * info.stride + x * 2 + 1] & 0xF0) >> 4;
				(*image)(x, y) = (r * 11 + b * 16 + g * 5) / 32;
			}
		}
		break;

	case ANDROID_BITMAP_FORMAT_A_8:
		for (size_t x = 0; x < info.width; x++)
		{
			for (size_t y = 0; y < info.height; y++)
			{
				(*image)(x, y) = raw[y * info.stride + x];
			}
		}
		break;
		
	}
	AndroidBitmap_unlockPixels(env, jbitmap);
	return returnResult(env, image);
}

#endif
